package com.mysteel.product.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.mysteel.product.service.CategoryBrandRelationService;
import com.mysteel.product.vo.web.Catalog2Vo;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.mysteel.common.utils.PageUtils;
import com.mysteel.common.utils.Query;

import com.mysteel.product.dao.CategoryDao;
import com.mysteel.product.entity.CategoryEntity;
import com.mysteel.product.service.CategoryService;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

@Slf4j
@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {

    @Autowired
    private CategoryBrandRelationService categoryBrandRelationService;

    @Autowired
    private StringRedisTemplate redisTemplate;

    private static final String CATALOG_JSON_CACHE_KEY = "catalogJson";

    @Autowired
    private RedissonClient redissonClient;

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<CategoryEntity>()
        );

        return new PageUtils(page);
    }

    @Override
    public List<CategoryEntity> listWithTree() {
        List<CategoryEntity> categoryEntities = baseMapper.selectList(null);
        // 递归获取一级分类下的所有分类
        List<CategoryEntity> firstCategory = categoryEntities.stream()
                .filter(categoryEntity -> categoryEntity.getParentCid() == 0)
                .map(categoryEntity -> {
                    categoryEntity.setChildren(getChildren(categoryEntity,categoryEntities));
                    return categoryEntity;
                })
                .sorted(Comparator.comparingInt(c -> (c.getSort() == null ? 0 : c.getSort())))
                .collect(Collectors.toList());

        return firstCategory;
    }

    @Override
    public void removeMenuByIds(List<Long> ids) {
        // TODO 检查当前需要删除的菜单在其他地方是否被引用
        baseMapper.deleteBatchIds(ids);
    }

    /**
     * 根据catelogId查询其完整路径 [父、子、孙]
     * @param catelogId
     * @return
     */
    @Override
    public Long[] findCategoryPathById(Long catelogId) {
        List<Long> paths = new ArrayList<>();
        paths = getParentId(catelogId,paths);
        Collections.reverse(paths);
        return paths.toArray(new Long[3]);
    }

    /**
     * 当分类信息更新之后，缓存中的有关分类的信息全部失效，目前有两个缓存：1、CACHE_getLevel1Category 2、CACHE_getCatalogJson
     * 失效办法：
     * 1、使用@Caching注解：让多个缓存操作应用到同一个方法上
     *     @Caching(evict = {
     *             @CacheEvict(value = "category",key = "'getCatalogJson'"),
     *             @CacheEvict(value = "category",key = "'getLevel1Category'")
     *     })
     * 2、使用@CacheEvict(value = "category",allEntries = true)，allEntries = true：删除缓存分区内的所有数据
     */
    /**
     * 级联更新所有关联的数据
     * @param category
     */
    /*@Caching(evict = {
            @CacheEvict(value = "category",key = "'getCatalogJson'"),
            @CacheEvict(value = "category",key = "'getLevel1Category'")
    })*/
    @CacheEvict(value = "category",allEntries = true)
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void updateCascade(CategoryEntity category) {
        this.updateById(category);
        if (StringUtils.isNotEmpty(category.getName())){
            categoryBrandRelationService.updateCategoryNameCascade(category.getCatId(),category.getName());
        }

    }

    /**
     * @Cacheable({"category"}):代表该方法的返回结果需要缓存
     * 如果缓存中存在数据，则不会执行方法，直接返回数据，如果缓存中没数据，则执行方法，并将结果放在缓存中
     * 默认行为：
     * 1、如果缓存中存在数据，则不会执行方法
     * 2、key是默认自动生成的 --> 缓存的名字::SimpleKey []
     * 3、value的值：默认使用JDK序列化机制将序列化后的数据存入redis
     * 4、默认过期时间为 -1 （永不过期）
     * 自定义：
     * 1、指定生成缓存使用的key：key属性指定，接受SpEl表达式
     * 2、指定缓存数据的过期时间
     * 3、value使用json存储：
     * @return
     */
    @Cacheable(value = {"category"},key = "#root.method.name",sync = true)
    @Override
    public List<CategoryEntity> getLevel1Category() {
        System.out.println("调用数据库获取一级分类信息...");
        return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid",0));
    }

    @Cacheable(value = "category",key = "#root.methodName",sync = true)
    @Override
    public Map<String, List<Catalog2Vo>> getCatalogJson() {

        // todo 堆外内存溢出异常
        /**
         * 问题：
         * spring boot2.0 以后默认使用lettuce作为操作Redis的客户端，它使用netty通信，但由于lettuce有bug导致堆外内存溢出异常
         * netty如果没有指定堆外内存，它会默认之用 -Xmx100m的值作为最大堆外内存，内存没有及时释放
         * 可以通过-Dio.netty.maxDirectMemory进行设置
         * 解决方案：不能只使用-Dio.netty.maxDirectMemory调大堆外内存
         * 1、升级lettuce客户端(高版本的目前未成功解决)   2、使用jedis客户端操作Redis
         * 使用使用jedis客户端操作Redis存在的问题：
         * jedis与redis通讯速度较慢，可能会超时（默认3s）：设置 spring.redis.timeout 属性，将超时时间延长
         */
        /**
         * 缓存穿透：大量查询一个缓存和数据库中都不存在的值
         * 解决办法：不存在数据设置短过期时间、布隆过滤器
         * 缓存击穿：某个热点缓存失效之后，大量请求进来
         * 解决办法：设置热点数据永不过期、定时更新、互斥锁（重点）
         * 缓存雪崩：Redis中的key同时大面积失效，导致大量请求直接查库
         * 解决办法：设置有效期均匀分布、数据预热、保证Redis服务高可用
         */
        // 先查缓存
        /*Map<String, List<Catalog2Vo>> catalogJsonFromRedis = getCatalogJsonFromRedis();
        if (!CollectionUtils.isEmpty(catalogJsonFromRedis)){
            return catalogJsonFromRedis;
        }

        return getCatalogJsonFromDbWithRedisson();*/

        return getCatalogJsonFromDbUseSpringCache();

    }

    private Map<String, List<Catalog2Vo>> getCatalogJsonFromDbUseSpringCache(){
        log.info("缓存未命中，查询数据库...");
        // 优化方案，将多次查询变为一次
        List<CategoryEntity> allCategory = baseMapper.selectList(null);
        // 先获取一级频道
        List<CategoryEntity> level1Category = getCatalogByParentCid(allCategory, 0L);
        Map<String, List<Catalog2Vo>> collect = level1Category.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
            List<Catalog2Vo> catalog2Vos = new ArrayList<>();
            List<CategoryEntity> list = getCatalogByParentCid(allCategory, v.getCatId());
            if (!CollectionUtils.isEmpty(list)) {
                catalog2Vos = list.stream().map(item -> {
                    Catalog2Vo catalog2Vo = new Catalog2Vo(v.getCatId().toString(), null, item.getCatId().toString(), item.getName());
                    // 找当前二级分类的三级分类信息
                    List<CategoryEntity> catalog3Entity = getCatalogByParentCid(allCategory, item.getCatId());
                    if (!CollectionUtils.isEmpty(catalog3Entity)) {
                        List<Catalog2Vo.Catalog3Vo> catalog3Vos = catalog3Entity.stream().map(category3Entity -> new Catalog2Vo.Catalog3Vo(item.getCatId().toString(), category3Entity.getCatId().toString(), category3Entity.getName())).collect(Collectors.toList());
                        catalog2Vo.setCatalog3List(catalog3Vos);
                    }
                    return catalog2Vo;
                }).collect(Collectors.toList());
            }
            return catalog2Vos;
        }));
        return collect;
    }

    /**
     * 使用Redisson分布式锁
     * 缓存与数据库中的数据如何保持一致：
     * 1、双写模式：修改数据的时候，既写数据库又写缓存
     * 2、失效模式：修改是数据库的时候让缓存失效，当下次查询的时候在设置缓存
     * 会存在问题：高并发情况下可能会存在数据不一致的问题
     * 解决方法：
     * 1、对于数据实时性和一致性要求不高的情况下，对缓存设置超时时间，超时之后再查数据库，更新缓存
     * 2、使用失效模式，配合分布式读写锁保证数据一致性
     * 3、canal自动同步mysql的bin log增量日志，在将数据更新至缓存，解决缓存一致性问题：中间件
     * @return
     */
    private Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithRedisson(){
        // 锁的粒度，如果粒度过大，可能会使请求响应太慢
        /**
         * 举例：1、根据不同商品ID设置不同缓存设置不同锁，product-10-lock product-11-lock 2、所有商品使用同一个锁 product-lock
         * 场景：如果大量请求进来，查询ID为10号商品的请求有10个，查询11号商品的请求有10万个
         * 如果使用方案1、则能很好的处理请求，如果使用方案2，则10个请求的返回需要等到10万个请求的锁释放后才能响应，明显不合理
         * 所以锁的粒度需要控制好
         */
        RLock lock = redissonClient.getLock("catalogJson-lock");

        lock.lock();
        log.info("获取分布式锁成功...");
        Map<String, List<Catalog2Vo>> catalogJsonFromDb = null;
        try {
            // 加锁成功，执行业务
            catalogJsonFromDb = getCatalogJsonFromDb();
        }catch (Exception e){
            log.error("业务执行过程中出现异常：{}",e);
        }finally {
            lock.unlock();
        }
        return catalogJsonFromDb;
    }

    /**
     * 使用分布式锁
     * @return
     */
    private Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithRedisLock(){
        ValueOperations<String, String> ops = redisTemplate.opsForValue();
        /**
         * 分布式锁：分布式下所有线程竞争同一把锁
         * redis:set key value NX（NX没有就设置，只有一个线程会成功）
         * 解锁使用脚本解锁
         */
        // 给锁设置唯一标识，防止当业务未执行完，锁过期，当业务执行完成之后，删除了其它线程的锁
        String uuid = UUID.randomUUID().toString();
        // 尝试获取锁（给Redis中以NX的方式设置一个值，设置成功即视为获取锁成功），设置过期时间，防止死锁（原子操作）
        if (ops.setIfAbsent("catalogJsonLock",uuid,120,TimeUnit.SECONDS)){
            log.info("获取分布式锁成功...");
            Map<String, List<Catalog2Vo>> catalogJsonFromDb = null;
            try {
                // 加锁成功，执行业务
                catalogJsonFromDb = getCatalogJsonFromDb();
            }catch (Exception e){
                log.error("业务执行过程中出现异常：{}",e);
            }finally {
                // 释放锁（非原子操作）
            /*String catalogJsonLock = ops.get("catalogJsonLock");
            if (uuid.equals(catalogJsonLock)){
                redisTemplate.delete("catalogJsonLock");
            }*/

                // Lua脚本解锁，原子操作
                String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
                redisTemplate.execute(new DefaultRedisScript<>(script,Long.class), Collections.singletonList("catalogJsonLock"),uuid);
            }
            return catalogJsonFromDb;
        }else {
            // 加锁失败...重试，自旋方式
            log.info("获取分布式锁失败...");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return getCatalogJsonFromDbWithRedisLock();
        }

    }

    /**
     * 使用本地锁
     * @return
     */
    private Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithLocalLock(){
        /**
         * 为什么能用this作为锁：因为spring boot默认使用的是单例模式
         * 为防止缓存击穿加互斥锁(先使用本地锁)
         * 问题：在分布式的情况下只能锁住当前进程
         *
         * 分布式锁：分布式下所有线程竞争同一把锁
         * redis:set key value NX（没有就设置，只有一个线程会成功）
         */
        synchronized (this){
            return getCatalogJsonFromDb();
        }
    }

    private Map<String, List<Catalog2Vo>> getCatalogJsonFromDb(){
        ValueOperations<String, String> ops = redisTemplate.opsForValue();
        // 加锁之后，进来先判断一下目前缓存是否存在，避免之前有线程获取到锁，查库之后设置了缓存
        Map<String, List<Catalog2Vo>> catalogJsonFromRedisTwice = getCatalogJsonFromRedis();
        if (!CollectionUtils.isEmpty(catalogJsonFromRedisTwice)) {
            return catalogJsonFromRedisTwice;
        }
        log.info("缓存未命中，查询数据库...");
        // 优化方案，将多次查询变为一次
        List<CategoryEntity> allCategory = baseMapper.selectList(null);
        // 先获取一级频道
        List<CategoryEntity> level1Category = getCatalogByParentCid(allCategory, 0L);
        Map<String, List<Catalog2Vo>> collect = level1Category.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
            List<Catalog2Vo> catalog2Vos = new ArrayList<>();
            List<CategoryEntity> list = getCatalogByParentCid(allCategory, v.getCatId());
            if (!CollectionUtils.isEmpty(list)) {
                catalog2Vos = list.stream().map(item -> {
                    Catalog2Vo catalog2Vo = new Catalog2Vo(v.getCatId().toString(), null, item.getCatId().toString(), item.getName());
                    // 找当前二级分类的三级分类信息
                    List<CategoryEntity> catalog3Entity = getCatalogByParentCid(allCategory, item.getCatId());
                    if (!CollectionUtils.isEmpty(catalog3Entity)) {
                        List<Catalog2Vo.Catalog3Vo> catalog3Vos = catalog3Entity.stream().map(category3Entity -> new Catalog2Vo.Catalog3Vo(item.getCatId().toString(), category3Entity.getCatId().toString(), category3Entity.getName())).collect(Collectors.toList());
                        catalog2Vo.setCatalog3List(catalog3Vos);
                    }
                    return catalog2Vo;
                }).collect(Collectors.toList());
            }
            return catalog2Vos;
        }));
        String catalogJson = JSON.toJSONString(collect);
        ops.set(CATALOG_JSON_CACHE_KEY, catalogJson, 5L, TimeUnit.MINUTES);
        return collect;
    }

    private Map<String, List<Catalog2Vo>> getCatalogJsonFromRedis(){
        ValueOperations<String, String> ops = redisTemplate.opsForValue();
        String catalogJsonString = ops.get(CATALOG_JSON_CACHE_KEY);

        if (StringUtils.isNotBlank(catalogJsonString)){
            log.info("使用Redis缓存数据，key：{}",CATALOG_JSON_CACHE_KEY);
            return JSON.parseObject(catalogJsonString, new TypeReference<Map<String, List<Catalog2Vo>>>() {
            });
        }
        return null;
    }

    private List<CategoryEntity> getCatalogByParentCid(List<CategoryEntity> allCategory,Long parentCid) {
        return allCategory.stream().filter(item -> item.getParentCid().equals(parentCid)).collect(Collectors.toList());
    }

    private List<Long> getParentId(Long catelogId,List<Long> paths){
        paths.add(catelogId);
        CategoryEntity byId = getById(catelogId);
        if (byId.getParentCid() != 0){
            getParentId(byId.getParentCid(),paths);
        }
        return paths;
    }

    // 递归查找当前菜单的子菜单
    private List<CategoryEntity> getChildren(CategoryEntity root,List<CategoryEntity> all){
        List<CategoryEntity> children = all.stream()
                .filter(categoryEntity -> categoryEntity.getParentCid().equals(root.getCatId()))
                .map(categoryEntity -> {
                    categoryEntity.setChildren(getChildren(categoryEntity, all));
                    return categoryEntity;
                })
                .sorted(Comparator.comparingInt(c -> (c.getSort() == null ? 0 : c.getSort())))
                .collect(Collectors.toList());
        return children;
    }

}